package huzh.foundation.util;

import java.lang.reflect.Method;
import java.lang.reflect.ReflectPermission;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.Vector;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 *<p>title:JavaBeanClass</p>
 *<p>description:</p>
 * @since 2014-6-8
 * @author huzh
 * @version 1.0
 */
public class JavaBeanClass {
    /**log*/
    private static final transient Log log = LogFactory.getLog(JavaBeanClass.class.getName());

    /**cacheEnabled*/
    private static boolean cacheEnabled = true;
    /**EMPTY_STRING_ARRAY*/
    private static final String[] EMPTY_STRING_ARRAY = new String[0];
    /**SIMPLE_TYPE_SET*/
    private static final Set<Object> SIMPLE_TYPE_SET = new HashSet<Object>();
    /**CLASS_INFO_MAP*/
    private static final Map<Object, Object> CLASS_INFO_MAP = Collections.synchronizedMap(new HashMap<Object, Object>());

    /**NO_ARGUMENTS*/
    private static final Object[] NO_ARGUMENTS = new Object[0];
    /**className*/
    private String className;
    /**readablePropertyNames*/
    private String[] readablePropertyNames = EMPTY_STRING_ARRAY;
    /**writeablePropertyNames*/
    private String[] writeablePropertyNames = EMPTY_STRING_ARRAY;
    /**setMethods*/
    private HashMap<Object, Object> setMethods = new HashMap<Object, Object>();
    /**getMethods*/
    private HashMap<Object, Object> getMethods = new HashMap<Object, Object>();
    /**setTypes*/
    private HashMap<Object, Object> setTypes = new HashMap<Object, Object>();
    /**getTypes*/
    private HashMap<Object, Object> getTypes = new HashMap<Object, Object>();

    /**
     * @param clazz
     */
    @SuppressWarnings("unchecked")
    private JavaBeanClass(Class clazz){
        this.className = clazz.getName();
        addMethods(clazz);
        this.readablePropertyNames = ((String[])(String[])this.getMethods.keySet()
                .toArray(new String[this.getMethods.keySet().size()]));
        this.writeablePropertyNames = ((String[])(String[])this.setMethods.keySet()
                .toArray(new String[this.setMethods.keySet().size()]));
    }

    /**
     * @param cls
     */
    @SuppressWarnings("unchecked")
    private void addMethods(Class cls){
        Method[] methods = getAllMethodsForClass(cls);
        for (int i = 0; i < methods.length; i++) {
            String name = methods[i].getName();
            if ((name.startsWith("set")) && (name.length() > 3)) {
                if (methods[i].getParameterTypes().length == 1) {
                    name = dropCase(name);
                    if (this.setMethods.containsKey(name)){
                        log.error("Illegal overloaded setter method for property " + name + " in class " + cls.getName()
                                + ".  This breaks the JavaBeans specification and can cause unpredicatble results.");
                    }
                    this.setMethods.put(name, methods[i]);
                    this.setTypes.put(name, methods[i].getParameterTypes()[0]);
                }
            } else if ((name.startsWith("get")) && (name.length() > 3)) {
                if (methods[i].getParameterTypes().length == 0) {
                    name = dropCase(name);
                    this.getMethods.put(name, methods[i]);
                    this.getTypes.put(name, methods[i].getReturnType());
                }
            } else if ((name.startsWith("is")) && (name.length() > 2) && 
                    (methods[i].getParameterTypes().length == 0)) {
                name = dropCase(name);
                this.getMethods.put(name, methods[i]);
                 this.getTypes.put(name, methods[i].getReturnType());
            }

            name = null;
        }
    }

    /**
     * @param cls
     * @return
     */
    @SuppressWarnings("unchecked")
    private Method[] getAllMethodsForClass(Class cls) {
        if (cls.isInterface()){
            return cls.getMethods();
        }
        return getClassMethods(cls);
    }

    /**
     * @param cls
     * @return
     */
    @SuppressWarnings("unchecked")
    private Method[] getClassMethods(Class cls){
        HashMap uniqueMethods = new HashMap();
        Class currentClass = cls;
        while (currentClass != null) {
            addUniqueMethods(uniqueMethods, currentClass.getDeclaredMethods());
            Class[] interfaces = currentClass.getInterfaces();
            for (int i = 0; i < interfaces.length; i++) {
                addUniqueMethods(uniqueMethods, interfaces[i].getMethods());
            }
            currentClass = currentClass.getSuperclass();
        }
        Collection methods = uniqueMethods.values();
        return (Method[])(Method[])methods.toArray(new Method[methods.size()]);
    }

    /**
     * @param uniqueMethods
     * @param methods
     */
    private void addUniqueMethods(HashMap<Object, Object> uniqueMethods, Method[] methods) {
        for (int i = 0; i < methods.length; i++) {
            Method currentMethod = methods[i];
            String signature = getSignature(currentMethod);
            if (!uniqueMethods.containsKey(signature)) {
                if (canAccessPrivateMethods()) {
                    try {
                        currentMethod.setAccessible(true);
                    } catch (Exception e){}
                }
                uniqueMethods.put(signature, currentMethod);
            }
        }
    }

    /**
     * @param method
     * @return
     */
    @SuppressWarnings("unchecked")
    private String getSignature(Method method) {
        StringBuffer sb = new StringBuffer();
        sb.append(method.getName());
        Class[] parameters = method.getParameterTypes();

        for (int i = 0; i < parameters.length; i++) {
            if (i == 0) {
                sb.append(':');
            } else {
                sb.append(',');
            }
            sb.append(parameters[i].getName());
        }
        return sb.toString();
    }

    /**
     * @return
     */
    private boolean canAccessPrivateMethods() {
        try {
            System.getSecurityManager().checkPermission(new ReflectPermission("suppressAccessChecks"));
            return true;
        } catch (SecurityException e) {
            return false;
        } catch (NullPointerException e) {
        }
         return true;
    }

    /**
     * @param name
     * @return
     */
    private static String dropCase(String name){
        if (name.startsWith("is")) {
            name = name.substring(2);
        } else if ((name.startsWith("get")) || (name.startsWith("set"))) {
            name = name.substring(3);
        } else {
            throw new RuntimeException("Error parsing property name '" + name 
                    + "'.  Didn't start with 'is', 'get' or 'set'.");
        }

        if ((name.length() == 1) || ((name.length() > 1) && (!Character.isUpperCase(name.charAt(1))))){
            name = name.substring(0, 1).toLowerCase(Locale.US) + name.substring(1);
            }
        return name;
    }

    /**
     * @return
     */
    public String getClassName(){
        return this.className;
    }

    /**
     * @param propertyName
     * @return
     */
    public Method getSetter(String propertyName) {
        for (Iterator<Object> i = this.setMethods.keySet().iterator(); i.hasNext(); ) {
            String temp = (String)i.next();
            if (temp.equalsIgnoreCase(propertyName)) {
                  return (Method)this.setMethods.get(temp);
            }
        }
      return null;
    }

    /**
     * @param propertyName
     * @return
     */
    public Method getGetter(String propertyName){
        for (Iterator<Object> i = this.getMethods.keySet().iterator(); i.hasNext(); ) {
            String temp = (String)i.next();
            if (temp.equalsIgnoreCase(propertyName)) {
                return (Method)this.getMethods.get(temp);
            }
        }
        return null;
    }

    /**
     * @param propertyName
     * @return
     */
    @SuppressWarnings("unchecked")
    public Class getSetterType(String propertyName) {
        for (Iterator<Object> i = this.setTypes.keySet().iterator(); i.hasNext(); ) {
            String temp = (String)i.next();
            if (temp.equalsIgnoreCase(propertyName)) {
                return (Class)this.setTypes.get(temp);
            }
        }
        return null;
    }

    /**
     * @param propertyName
     * @return
     */
    @SuppressWarnings("unchecked")
    public Class getGetterType(String propertyName){
        for (Iterator<Object> i = this.getTypes.keySet().iterator(); i.hasNext(); ) {
            String temp = (String)i.next();
            if (temp.equalsIgnoreCase(propertyName)) {
                return (Class)this.getTypes.get(temp);
            }
        }
        return null;
    }

    /**
     * @return
     */
    public String[] getReadablePropertyNames(){
        return this.readablePropertyNames;
    }

    /**
     * @return
     */
    public String[] getWriteablePropertyNames(){
        return this.writeablePropertyNames;
    }

    /**
     * @param propertyName
     * @return
     */
    public boolean hasWritableProperty(String propertyName){
        return this.setMethods.keySet().contains(propertyName);
    }

    /**
     * @param propertyName
     * @return
     */
    public boolean hasReadableProperty(String propertyName){
        return this.getMethods.keySet().contains(propertyName);
    }

    /**
     * @param clazz
     * @return
     */
    @SuppressWarnings("unchecked")
    public static boolean isKnownType(Class clazz) {
        if (SIMPLE_TYPE_SET.contains(clazz)) {
            return true;
        }
        if (Collection.class.isAssignableFrom(clazz)) {
            return true;
        }
        if (Map.class.isAssignableFrom(clazz)) {
            return true;
        }
        if (List.class.isAssignableFrom(clazz)) {
            return true;
        }
        if (Set.class.isAssignableFrom(clazz)) {
            return true;
        }

         return Iterator.class.isAssignableFrom(clazz);
    }

    /**
     * @param clazz
     * @return
     */
    @SuppressWarnings("unchecked")
    public static JavaBeanClass getInstance(Class clazz) {
        if (cacheEnabled) {
            synchronized (clazz) {
                JavaBeanClass cache = (JavaBeanClass)CLASS_INFO_MAP.get(clazz);
                if (cache == null) {
                    cache = new JavaBeanClass(clazz);
                    CLASS_INFO_MAP.put(clazz, cache);
                }
                return cache;
            }
        }
        return new JavaBeanClass(clazz);
    }

    /**
     * @param ca
     */
    public static void setCacheEnabled(boolean ca){
        cacheEnabled = ca;
    }

    /**
     * @param object
     * @param name
     * @return
     * @throws Exception
     */
    public static Object getProperty(Object object, String name) throws Exception{
        JavaBeanClass classCache = getInstance(object.getClass());
        Object value = null;
        Method method = classCache.getGetter(name);
        if (method == null) {
            throw new NoSuchMethodException("No GET method for property " + name + " on instance of " 
                    + object.getClass().getName());
        }

        value = method.invoke(object, NO_ARGUMENTS);
        return value;
    }

    /**
     * @param object
     * @param name
     * @param value
     * @throws Exception
     */
    @SuppressWarnings("unchecked")
    public static void setProperty(Object object, String name, Object value) throws Exception{
        JavaBeanClass classCache = getInstance(object.getClass());
        Method method = classCache.getSetter(name);
        if (method == null) {
            throw new NoSuchMethodException("No SET method for property " + name + " on instance of " 
                    + object.getClass().getName());
        }

        Object[] params = new Object[1];
        Class trueType = classCache.getSetterType(name);
        params[0] = transToTrueType(trueType, value);
        method.invoke(object, params);
    }

    /**
     * @param clz
     * @param value
     * @return
     */
    @SuppressWarnings("unchecked")
    private static Object transToTrueType(Class clz, Object value) {
        if (clz.isArray()) {
            if ((value instanceof byte[])) {
                 return value;
            }
            return String.valueOf(value).getBytes();
        }

        String clName = clz.getName();
        if (clName.equals("java.lang.String")) {
            return String.valueOf(value);
        }
        if ((clName.equalsIgnoreCase("int")) || (clName.equals("java.lang.Integer"))) {
            return Integer.valueOf(String.valueOf(value));
        }
        if ((clName.equalsIgnoreCase("short")) || (clName.equals("java.lang.Short"))) {
            return Short.valueOf(String.valueOf(value));
        }
        if ((clName.equalsIgnoreCase("long")) || (clName.equals("java.lang.Long"))) {
            return Long.valueOf(String.valueOf(value));
        }
        if ((clName.equalsIgnoreCase("float")) || (clName.equals("java.lang.Float"))) {
            return Float.valueOf(String.valueOf(value));
        }
        if ((clName.equalsIgnoreCase("double")) || (clName.equals("java.lang.Double"))) {
            return Double.valueOf(String.valueOf(value));
        }
        if (clName.equals("java.lang.BigInteger")) {
            new BigInteger(String.valueOf(value));
        }
        else if (clName.equals("java.lang.BigDecimal")) {
            new BigDecimal(String.valueOf(value));
        }
        else if ((clName.equalsIgnoreCase("boolean")) || (clName.equals("java.lang.Boolean"))) {
            return Boolean.valueOf(SysUtil.getBoolean(String.valueOf(value)));
        }
        return value;
    }

    static{
        SIMPLE_TYPE_SET.add(String.class);
        SIMPLE_TYPE_SET.add(Byte.class);
        SIMPLE_TYPE_SET.add(Short.class);
        SIMPLE_TYPE_SET.add(Character.class);
        SIMPLE_TYPE_SET.add(Integer.class);
        SIMPLE_TYPE_SET.add(Long.class);
        SIMPLE_TYPE_SET.add(Float.class);
        SIMPLE_TYPE_SET.add(Double.class);
        SIMPLE_TYPE_SET.add(Boolean.class);
        SIMPLE_TYPE_SET.add(Date.class);
        SIMPLE_TYPE_SET.add(Class.class);
        SIMPLE_TYPE_SET.add(BigInteger.class);
        SIMPLE_TYPE_SET.add(BigDecimal.class);

        SIMPLE_TYPE_SET.add(Collection.class);
        SIMPLE_TYPE_SET.add(Set.class);
        SIMPLE_TYPE_SET.add(Map.class);
        SIMPLE_TYPE_SET.add(List.class);
        SIMPLE_TYPE_SET.add(HashMap.class);
        SIMPLE_TYPE_SET.add(TreeMap.class);
        SIMPLE_TYPE_SET.add(ArrayList.class);
        SIMPLE_TYPE_SET.add(LinkedList.class);
        SIMPLE_TYPE_SET.add(HashSet.class);
        SIMPLE_TYPE_SET.add(TreeSet.class);
        SIMPLE_TYPE_SET.add(Vector.class);
        SIMPLE_TYPE_SET.add(Hashtable.class);
        SIMPLE_TYPE_SET.add(Enumeration.class);
    }
}
